/**
 IDEA Configurator
 Copyright (C) 2006 Justin Tomich<tomichj at gmail dot com>

 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License as
 published by the Free Software Foundation; either version 2 of the
 License, or (at your option) any later version.

 This program is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

package configurator.task;

import configurator.ModuleBuilder;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.w3c.dom.Document;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;


/**
 * Ant task to build a Module configuration file.
 * <p/>
 * todo adding setting to inherit output dirs from project, IDEA 6+ only
 *
 * @author Justin Tomich
 */
public class IdeaModuleTask extends Task
{
    // todo remove default source/javadoc/javadocurl, ensure a null means "none"
    private String sourceProperty = null; // default value
    private String javadocProperty = null; // default value
    private String javadocUrlProperty = null; // default value

    private File rootDir;
    private boolean excludeOutputPaths;
    private File moduleFile;
    private boolean relativePaths = true; // defaults to true yo!
    private Path sources = new Path(getProject());
    private Path tests = new Path(getProject());
    private Path excludes = new Path(getProject());
    private File outputDir;
    private File testsOutputDir;
    private Path classpath = new Path(getProject());
    private List<ClasspathFilterSubtask> filters =
            new ArrayList<ClasspathFilterSubtask>();

    private List<DependencySubtask> dependencies =
            new ArrayList<DependencySubtask>();

    // two ways to associate javadoc and source with library jars
    // * subtask, lib -> javadoc
    // * search 4 property name matching jar name, eg javadoc.ajax-1.1.jar
    // ** optionally specify property prepended to javadoc and source properties


    /**
     * Set the root directory of the Module. Not a required setting. Defaults to
     * ant's notion of the base dir.
     *
     * @param rootDir root directory of Module, as a File.
     */
    public void setRootDir(File rootDir)
    {
        this.rootDir = rootDir;
    }


    /**
     * Not required to be set, defaults to false.
     *
     * @param excludeOutputPaths true if output paths should be excluded
     */
    public void setExcludeOutputPaths(boolean excludeOutputPaths)
    {
        this.excludeOutputPaths = excludeOutputPaths;
    }


    /**
     * Set string that source properties must begin with to be automagically
     * identified as source for a corresponging library. Other ant properties
     * beginning with this string will be treated as a source path for a library
     * jar of the name: sourceProperty + "." + path-to-jar.
     * <p/>
     * Example: sourceProperty is set to "source". A jar in the classpath has
     * the path "/lib/some-library-1.0.jar". If another property exists with the
     * name "source./lib/some-library-1.0.jar", the source property will be
     * treated as the source accompanying "some-library-1.0.jar".
     *
     * @param sourceProperty
     */
    public void setSourceProperty(String sourceProperty)
    {
        this.sourceProperty = sourceProperty;
    }


    /**
     * Set string javadoc properties must begin with to be automagically picked
     * up.
     *
     * @param javadocProperty properties
     * @see #setSourceProperty(String)
     */
    public void setJavadocProperty(String javadocProperty)
    {
        this.javadocProperty = javadocProperty;
    }


    /**
     * Set the string javadoc url properties must begin with.
     *
     * @param javadocUrlProperty javadoc properties begin with this String
     * @see #setSourceProperty(String)
     */
    public void setJavadocUrlProperty(String javadocUrlProperty)
    {
        this.javadocUrlProperty = javadocUrlProperty;
    }


    /**
     * Set the File for the module file to be written to. If null, a default
     * will be chosen based on the ant project name.
     *
     * @param moduleFile location of module configuration file to generate.
     */
    public void setModuleFile(File moduleFile)
    {
        if (!moduleFile.getAbsolutePath().endsWith(".iml"))
        {
            String p = moduleFile.getAbsolutePath() + ".iml";
            this.moduleFile = new File(p);
        }
        else
        {
            this.moduleFile = moduleFile;
        }
    }


    /**
     * Use relative paths for files outside module directory?
     *
     * @param relativePaths true if paths outside module dir should be relative
     */
    public void setRelativePaths(boolean relativePaths)
    {
        this.relativePaths = relativePaths;
    }


    public void setSrcDir(File sourceDir)
    {
        sources.setLocation(sourceDir);
    }


    public void setSrcPathRef(Reference srcPathRef)
    {
        Path p = (Path) srcPathRef.getReferencedObject();
        sources.append(p);
    }


    public Path createSrc()
    {
        return sources.createPath();
    }


    public void setTestsDir(File testDir)
    {
        tests.setLocation(testDir);
    }


    public void setTestsPathRef(Reference testsPathRef)
    {
        Path p = (Path) testsPathRef.getReferencedObject();
        tests.append(p);
    }


    public Path createTests()
    {
        return tests.createPath();
    }


    /**
     * @param outputDir
     */
    public void setOutputDir(File outputDir)
    {
        this.outputDir = outputDir;
    }


    public void setTestsOutputDir(File testsOutputDir)
    {
        this.testsOutputDir = testsOutputDir;
    }


    public void setExcludes(File excludes)
    {
        this.excludes.setLocation(excludes);
    }


    public void setExcludesPathRef(Reference excludesPathRef)
    {
        Path p = (Path) excludesPathRef.getReferencedObject();
        excludes.append(p);
    }


    public Path createExcludes()
    {
        return excludes.createPath();
    }


    public void setClasspath(Path classpath)
    {
        this.classpath.append(classpath);
    }


    /**
     * Order entries are set in here.
     *
     * @return OrderEntriesSubtask to receive orderEntry configuration.
     */
    public DependenciesSubtask createDependencies()
    {
        // just a thin venier, we don't even retain it
        return new DependenciesSubtask();
    }


    /**
     * Filter any classpath entries matching the given pattern.
     */
    public ClasspathFilterSubtask createClasspathFilter()
    {
        ClasspathFilterSubtask filter = new ClasspathFilterSubtask();
        filters.add(filter);
        return filter;
    }


    /**
     * Execute the task, generating an ant module configuration file.
     *
     * @throws BuildException
     */
    public void execute() throws BuildException
    {
        // order matters
        super.execute();
        ModuleBuilder builder = new ModuleBuilder(moduleFile(), rootDir(),
                relativePaths);
        builder.setExcludeOutputPaths(excludeOutputPaths);
        if (sources.size() > 0) builder.addSources(sources.list());
        if (tests.size() > 0) builder.addTests(tests.list());
        if (excludes.size() > 0) builder.addExcludes(excludes.list());
        if (outputDir != null) builder.setOutput(outputDir.getAbsolutePath());
        if (testsOutputDir != null)
        {
            builder.setOutputTest(testsOutputDir.getPath());
        }

        // convert classpath to a Dependency... it'll get all the
        // magic and be able to add itself to the builder that way.
        if (classpath.size() > 0)
        {
            ModuleLibraryClasspath cp =
                    new ModuleLibraryClasspath(getProject(),
                            sourceProperty, javadocProperty,
                            javadocUrlProperty);
            cp.append(classpath);
            dependencies.add(cp);
        }

        for (DependencySubtask entry : dependencies)
        {
            entry.addTo(builder);
        }

        for (ClasspathFilterSubtask filter : filters)
        {
            filter.addTo(builder);
        }

        try
        {
            writeBuildFile(builder.toDom(), moduleFile());
        }
        catch (ParserConfigurationException e)
        {
            throw new BuildException(e);
        }
    }


    private File rootDir()
    {
        if (rootDir != null) return rootDir;
        return getProject().getBaseDir();
    }


    private String moduleFile()
    {
        if (moduleFile != null) return moduleFile.getAbsolutePath();

        // default is ant project name, .iws, in the base dir.
        Project p = getProject();
        String name = p.getName();
        File baseDir = p.getBaseDir();
        return new File(baseDir, name + ".iml").getAbsolutePath();
    }


    private void writeBuildFile(Document dom, String pathToFile)
            throws BuildException
    {
        String buildFile = serializeByTransformer(dom);
        writeToFile(buildFile, pathToFile);
    }


    protected File writeToFile(String contents, String pathToFile)
    {
        File f = new File(pathToFile);
        FileWriter writer;
        try
        {
            writer = new FileWriter(f);
            writer.write(contents);
            writer.flush();
            return f;
        }
        catch (IOException e)
        {
            throw new BuildException(e);
        }
    }


    /**
     * Pretty-print xml, as much as it possible with the included DOM
     * implementation. Generally this gets you appropriate newlines but not
     * indentation.
     *
     * @param doc DOM to serialize to text.
     * @return nicely formatted XML as a String
     * @throws BuildException
     */
    private String serializeByTransformer(Document doc) throws BuildException
    {
        OutputStream out = new ByteArrayOutputStream();
        TransformerFactory tfactory = TransformerFactory.newInstance();
        Transformer serializer;
        try
        {
            serializer = tfactory.newTransformer();
            //Setup indenting to "pretty print"
            serializer.setOutputProperty(OutputKeys.INDENT, "yes");
            serializer.setOutputProperty(
                    "{ http://xml.apache.org/xslt }indent-amount", "2");
            serializer.transform(new DOMSource(doc), new StreamResult(out));
            return out.toString();
        }
        catch (TransformerException e)
        {
            // this is fatal, just dump the stack and throw an exception
            e.printStackTrace();
            throw new BuildException(e);
        }
    }


    /**
     * Thin shell around the entries, we don't even keep a ref to it, just pump
     * the entries straight into the entries list.
     */
    public class DependenciesSubtask
    {
        public ModuleLibrarySubtask createModuleLibrary()
        {
            ModuleLibrarySubtask lib = new ModuleLibrarySubtask(getProject());
            dependencies.add(lib);
            return lib;
        }


        public ProjectLibrarySubtask createProjectLibrary()
        {
            ProjectLibrarySubtask lib = new ProjectLibrarySubtask();
            dependencies.add(lib);
            return lib;
        }


        public GlobalLibrarySubtask createGlobalLibrary()
        {
            GlobalLibrarySubtask lib = new GlobalLibrarySubtask();
            dependencies.add(lib);
            return lib;
        }


        public JdkSubtask createJdk()
        {
            JdkSubtask jdk = new JdkSubtask();
            dependencies.add(jdk);
            return jdk;
        }


        /**
         * Entry for the module's source.
         */
        public ModuleSourceSubtask createModuleSource()
        {
            ModuleSourceSubtask source = new ModuleSourceSubtask();
            dependencies.add(source);
            return source;
        }


        public ModuleSubtask createModule()
        {
            ModuleSubtask module = new ModuleSubtask();
            dependencies.add(module);
            return module;
        }


        public Path createClasspath()
        {
            // maintain order of this dependency by creating
            // ModuleLibraryClasspath and adding to dependencies
            // (an ordered list)
            ModuleLibraryClasspath p = new ModuleLibraryClasspath(getProject(),
                    sourceProperty, javadocProperty, javadocUrlProperty);
            dependencies.add(p);
            return p;
        }

    }


    public class ClasspathFilterSubtask
    {
        private String pattern;


        public void setPattern(String pattern)
        {
            this.pattern = pattern;
        }


        protected void addTo(ModuleBuilder builder)
        {
            builder.addClasspathFilter(pattern);
        }
    }
}
